package com.hzgj.bcl.soa.command;

import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;
import com.hzgj.bcl.soa.collector.Collectors;
import com.hzgj.bcl.soa.collector.JmxCollector;
import com.hzgj.bcl.soa.config.ServiceDef;
import com.hzgj.bcl.soa.registry.Provider;
import com.hzgj.bcl.soa.util.Constants;
import com.hzgj.bcl.util.IocContext;
import com.hzgj.bcl.util.lang.StrKit;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import static com.hzgj.bcl.soa.command.CommandsInitializer.CLIENT_COMMAND;
import static com.hzgj.bcl.soa.command.CommandsInitializer.REG_COMMAND;
import static com.hzgj.bcl.util.lang.ThreadPoolKit.getScheduledExecutorService;

/**
 * Created by Administrator on 2016/8/9.
 */
public class RegistryCommand implements SoaCommand {

    private Logger logger = LoggerFactory.getLogger(getClass());
    EtcdClient etcdClient;
    public static final String SERVICE = "service";
    public static final String COMMAND_DELETE = "delete";
    public static final String COMMAND_NODES = "nodes";
    public static final String COMMAND_NODE_SERVICES = "node-service";
    @Getter
    Map<String, ServiceDef.ServiceInfo> serviceInfos = Maps.newHashMap();
    Map<ServerInfo, Map<String, NodeProvider>> servers = Maps.newHashMap();

    @Override
    public String invoke(CommandOptions commandOptions) {
        String[] service_param = commandOptions.params.get(SERVICE);
        String service = null;
        if (service_param != null && StrKit.notBlank(service_param[0])) {
            service = service_param[0].trim();
        }
        String[] secondCommand = commandOptions.params.get(SoaCommand.secondCommand);
        if (StrKit.notBlank(secondCommand)) {
            switch (secondCommand[0]) {
                case COMMAND_DELETE:
                    return delete(service);
                case COMMAND_NODES:
                    return getNodeList();
                case COMMAND_NODE_SERVICES:
                    String[] address = commandOptions.params.get("address");
                    return getServicesByNode(address[0]);
                default:
                    return defaultOp(service);
            }
        } else {
            return defaultOp(service);
        }
    }

    public Collection<Provider> getProviders(String service) {
        return etcdClient.getProviders(service).values();
    }

    @Override
    public String getName() {
        return REG_COMMAND;
    }

    @Override
    public void start() {
        etcdClient = IocContext.applicationContext.getBean(EtcdClient.class);
        ScheduledExecutorService scheduledExecutorService = getScheduledExecutorService("cache-registry-serviceInfo");
        scheduledExecutorService.scheduleWithFixedDelay(() -> {
            try {
                long start = System.currentTimeMillis();
                ClientCommand clientCommand = CommandsInitializer.getCommand(CLIENT_COMMAND);
                Map<String, ServiceDef.ServiceInfo> serviceInfos = etcdClient.getServices(service -> clientCommand.getCount(service));
                logger.info("get service info spend " + (System.currentTimeMillis() - start) + "ms");
                this.serviceInfos = serviceInfos;
                start = System.currentTimeMillis();
                handleServerInfo(serviceInfos);
                logger.info("handle server info " + (System.currentTimeMillis() - start) + "ms");
            } catch (Exception e) {
                logger.error("{}", e);
            }
        }, 0, 5, TimeUnit.SECONDS);
    }

    private String delete(String service) {
        Map<String, Object> result = Maps.newHashMap();
        if (StrKit.isBlank(service)) {
            result.put("success", false);
            result.put("msg", "service cant't be null");
        } else {
            try {
                etcdClient.remService(service);
                serviceInfos.remove(service);
                result.put("success", true);
            } catch (Exception e) {
                logger.error("{}", e);
                result.put("success", false);
                result.put("msg", e.getMessage());
            }
        }
        return JSON.toJSONString(result);
    }

    private String defaultOp(String service) {
        if (StrKit.isBlank(service)) {
            Map map = Maps.newHashMap();
            map.put("data", serviceInfos.values());
            return JSON.toJSONString(map);
        } else {
            return JSON.toJSONString(getProviders(service));
        }
    }

    private String getNodeList() {
        Map map = Maps.newHashMap();
        map.put("data", servers.keySet());
        return JSON.toJSONString(map);
    }

    public Collection<NodeProvider> getNodeServices(String address) {
        Map<String, NodeProvider> providers = servers.get(ServerInfo.builder().address(address).build());
        if (providers != null) {
            return providers.values();
        }
        return Collections.emptyList();
    }

    private String getServicesByNode(String address) {
        Map map = Maps.newHashMap();
        map.put("data", getNodeServices(address));
        return JSON.toJSONString(map);
    }

    public void updateServiceInfo(String address, List<String> services) {
        Map<String, NodeProvider> pp = servers.get(ServerInfo.builder().address(address).build());
        if (pp == null) {
            return;
        }
        services.forEach(s -> {
            Map<String, Provider> providers = etcdClient.getProviders(s);
            pp.put(s, NodeProvider.builder().provider(providers.get(address)).serviceName(s).build());
        });
    }

    private void handleServerInfo(Map<String, ServiceDef.ServiceInfo> serviceInfos) {
        Map<ServerInfo, Map<String, NodeProvider>> servers = Maps.newHashMap();
        Map<String, ServerInfo> serverMapping = Maps.newHashMap();
        serviceInfos.values().forEach(serviceInfo -> {
            Map<String, Provider> providers = etcdClient.getProviders(serviceInfo.getName());
            for (Provider provider : providers.values()) {

                ServerInfo serverInfo = serverMapping.get(provider.getAddress());
                if (serverInfo == null) {
                    serverInfo = ServerInfo.builder().address(provider.getAddress()).jmxPort(provider.getJmxPort()).build();
                    serverMapping.putIfAbsent(provider.getAddress(), serverInfo);
                    String[] address = serverInfo.getAddress().split(":");
                    fillConnections(serverInfo, address[0], Integer.parseInt(address[1]), provider.getJmxPort(), 2);
                }
                servers.compute(serverInfo, (key, services) -> {
                    if (services == null) {
                        services = Maps.newHashMap();
                    }
                    if (provider.isUp()) {
                        key.addUp();
                    } else {
                        key.addDown();
                    }
                    services.put(serviceInfo.getName(), NodeProvider.builder().provider(provider).serviceName(serviceInfo.getName()).build());
                    return services;
                });
            }
        });
        this.servers = servers;
    }

    private void fillConnections(ServerInfo serverInfo, String address, int port, int jmxPort, int retryCount) {
        if (retryCount-- == 0) {
            return;
        }
        JmxCollector jmxCollector = Collectors.getCollector(address, jmxPort);
        if (jmxCollector != null) {
            try {
                serverInfo.setConnections(Long.parseLong(jmxCollector.invokeWithoutParam(Constants.getConnectionHandlerObjectName(port),
                        "getConnections").toString()));
            } catch (Exception e) {
                logger.error("{}", e);
                Collectors.removeCollector(address, jmxPort);
                fillConnections(serverInfo, address, port, jmxPort, retryCount);
            }
        }
    }


    @Builder
    @Getter
    public static class NodeProvider {
        Provider provider;
        String serviceName;
    }

    @Builder
    @Getter
    @Setter
    static class ServerInfo {
        String address;
        int upCount;
        int downCount;
        int jmxPort;
        long connections;

        public void addUp() {
            upCount++;
        }

        public void addDown() {
            downCount++;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            ServerInfo that = (ServerInfo) o;
            return Objects.equals(address, that.address);
        }

        @Override
        public int hashCode() {
            return Objects.hash(address);
        }
    }
}
